/*
 * TcpClient.js
 *
 * Copyright (C) 2012 - Dr.NP
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * TCP client based on HTML5 WebSocket
 * New Firefox && Chrome support only now.
 * 
 * @package bsp
 * @author Dr.NP <np@bsgroup.org>
 * @update 10/11/2012
 * @changelog
 *         [10/11/2012] - Creation
 */

/**
 * Main Client class
 */
function _u82str(ab)
{
    var binary_string = '';
    for (var i = 0; i < ab.length; i ++)
    {
        binary_string += String.fromCharCode(ab[i]);
    }
    
    return binary_string;
}

function TcpClient(host, port)
{
    // Constants
    this.DATA_TYPE_STREAM       = 0x0;
    this.DATA_TYPE_PACKET       = 0x1;
    
    this.PACKET_TYPE_REP        = 0x0;
    this.PACKET_TYPE_RAW        = 0x1;
    this.PACKET_TYPE_OBJ        = 0x2;
    this.PACKET_TYPE_CMD        = 0x3;
    this.PACKET_TYPE_HEARTBEAT  = 0x7;
    
    this.LENGTH_TYPE_32B        = 0x0;
    this.LENGTH_TYPE_64B        = 0x1;
    
    this.SERIALIZE_TYPE_NATIVE  = 0x0;
    this.SERIALIZE_TYPE_JSON    = 0x1;
    this.SERIALIZE_TYPE_MSGPACK = 0x2;
    this.SERIALIZE_TYPE_AMF     = 0x3;
    
    this.COMPRESS_TYPE_NONE     = 0x0;
    this.COMPRESS_TYPE_DEFLATE  = 0x1;
    this.COMPRESS_TYPE_LZO      = 0x2;
    this.COMPRESS_TYPE_SNAPPY   = 0x3;
    
    // Properties
    this.host                   = host;
    this.port                   = port;
    this.socket                 = null;
    this.on_connect             = null;
    this.on_close               = null;
    this.on_data                = null;
    this.on_close               = null;
    this.on_heartbeat           = null;
    this.heartbeat              = 0;
    this.heartbeat_failure      = 10;
    this.timer                  = null;
    this.recv_buffer            = null;
    this.pkts                   = new Array();
    this.stream                 = '';
    
    // Data type
    this.data_type = arguments[2] ? parseInt(arguments[2]) : this.DATA_TYPE_STREAM;
    
    // Heartbeat
    this.heartbeat = arguments[3] ? parseInt(arguments[3]) : 0;

    // Heartbeat failure check
    this.heartbeat_failure = arguments[4] ? parseInt(arguments[4]) : 10;
    return;
}

TcpClient.DATA_TYPE_STREAM = 0x0;
TcpClient.DATA_TYPE_PACKET = 0x1;

TcpClient.prototype.connect = function() {
    var addr = 'ws://' + this.host + ':' + this.port + '/';
    if ('MozWebSocket' in window)
    {
        // Mozilla
        this.socket = new MozWebSocket(addr);
    }
    
    else if ('WebSocket' in window)
    {
        // Webkit
        this.socket = new WebSocket(addr);
    }
    
    else
    {
        // WebSocket not supported
        alert('You browser does not support WebSocket');
        return;
    }
    
    if (!this.socket)
    {
        console.log('Create websocket connection error');
    }
    
    // Events and type
    this.socket.binaryType = "blob";
    var clt = this;
    var hb_hdr = new Uint8Array(1);
    hb_hdr[0] = (clt.PACKET_TYPE_HEARTBEAT << 5);
    var hb = new Blob([hb_hdr]);
    
    this.socket.onopen = function() {
        if (clt.heartbeat > 0)
        {
            clt.timer = window.setInterval(function() {
                clt.socket.send(hb);
            }, clt.heartbeat, null);
        }
        
        while (this.readyState != 1)
        {
            // Sleep
        }
        var hdr = new Uint8Array(1);
        hdr[0] = (clt.PACKET_TYPE_REP << 5) | (clt.LENGTH_TYPE_32B << 4) | (clt.SERIALIZE_TYPE_JSON << 2) | (clt.COMPRESS_TYPE_NONE);
        var req = new Blob([hdr]);
        this.send(req);
        
        if (typeof(clt.on_connect) == 'function')
        {
            clt.on_connect();
        }
    }
    this.socket.onclose = function() {
        if (clt.timer)
        {
            clearInterval(clt.timer);
        }
        if (typeof(clt.on_close) == 'function')
        {
            clt.on_close();
        }
    }
    this.socket.onmessage = function(e) {
        if (e.data instanceof Blob)
        {
            clt.recv_buffer = (clt.recv_buffer) ? new Blob([clt.recv_buffer, e.data]) : e.data;
            var reader = new FileReader();
            if (clt.DATA_TYPE_PACKET == clt.data_type)
            {
                reader.addEventListener('loadend', function() {
                    var buf = reader.result;
                    // Parse data
                    var curr = 0;
                    var remaining = buf.byteLength;
                    while (remaining > 0)
                    {
                        // Read first byte
                        var dv = new DataView(buf, curr);
                        var hdr = dv.getUint8(0);
                        var p_type = (hdr >> 5) & 7;
                        var l_type = (hdr >> 4) & 1;
                        var s_type = (hdr >> 2) & 3;
                        var c_type = (hdr) & 3;
                        var plen;
                        remaining --;
                        curr ++;
                        
                        if (clt.PACKET_TYPE_RAW == p_type || 
                            clt.PACKET_TYPE_OBJ == p_type || 
                            clt.PACKET_TYPE_CMD == p_type)
                        {
                            var plen;
                            if (clt.LENGTH_TYPE_32B == l_type)
                            {
                                if (remaining < 4)
                                {
                                    break;
                                }
                                plen = dv.getUint32(1, false);
                                remaining -= 4;
                                curr += 4;
                            }
                            else
                            {
                                if (remaining < 8)
                                {
                                    break;
                                }
                                plen = dv.getUint32(5, false);
                                remaining -= 8;
                                curr += 8;
                            }
                            if (remaining < plen)
                            {
                                break;
                            }
                            switch (p_type)
                            {
                                case clt.PACKET_TYPE_RAW : 
                                    clt.pkts.push({'type' : p_type, 'raw' : buf.slice(curr, curr + plen)});
                                    break;
                                case clt.PACKET_TYPE_OBJ : 
                                    if (clt.SERIALIZE_TYPE_JSON == s_type)
                                    {
                                        var arr = new Uint8Array(buf.slice(curr, curr + plen));
                                        clt.pkts.push({'type' : p_type, 'obj' : JSON.parse(_u82str(arr))});
                                    }
                                    break;
                                case clt.PACKET_TYPE_CMD : 
                                    if (clt.SERIALIZE_TYPE_JSON == s_type)
                                    {
                                        var dv = new DataView(buf, curr);
                                        var cmd = dv.getUint32(0, false);
                                        curr += 4;
                                        var arr = new Uint8Array(buf.slice(curr, curr + plen - 4));
                                        clt.pkts.push({'type' : p_type, 'cmd' : cmd, 'params' : JSON.parse(_u82str(arr))});
                                    }
                                    break;
                                default : 
                                    break;
                            }
                            remaining -= plen;
                            curr += plen;
                            if (typeof(clt.on_data) == 'function')
                            {
                                clt.on_data();
                            }
                        }
                        else if (clt.PACKET_TYPE_HEARTBEAT == p_type)
                        {
                            if (typeof(clt.on_heartbeat) == 'function')
                            {
                                clt.on_heartbeat();
                            }
                        }
                        
                        else if (clt.PACKET_TYPE_REP == p_type)
                        {
                            // Just ignore
                        }
                    }
                    // Slice buffer
                    if (0 == remaining)
                    {
                        clt.recv_buffer = '';
                    }
                    else
                    {
                        clt.recv_buffer = clt.recv_buffer.slice(0 - remaining);
                    }
                });
                reader.readAsArrayBuffer(clt.recv_buffer);
            }
            else
            {
                // Append to buffer only
                reader.addEventListener('loadend', function() {
                    clt.stream += reader.result;
                    clt.recv_buffer = null;
                });
                reader.readAsBinaryString(clt.recv_buffer);
            }
        }
    }
    
    this.socket.onerror = function(e) {
        if (clt.timer)
        {
            clearInterval(clt.timer);
        }
        if (typeof(clt.on_error) == 'function')
        {
            clt.on_error(e.data);
        }
    }
    return;
}

TcpClient.prototype.disconnect = function() {
    if (this.timer)
    {
        clearInterval(this.timer);
    }
    if (this.socket)
    {
        this.socket.close();
    }
    return;
}

TcpClient.prototype._send_stream = function(data) {
    if (this.socket.readyState != 1)
    {
        return 0;
    }
    
    var b = new Blob([data]);
    this.socket.send(b);
    return b.length;
}

TcpClient.prototype._send_raw = function(data) {
    if (this.socket.readyState != 1)
    {
        return 0;
    }
    
    var hdr = new Uint8Array(1);
    hdr[0] = (this.PACKET_TYPE_RAW << 5) | (this.LENGTH_TYPE_32B << 4);
    var cnt = new Blob([data]);
    var len = new ArrayBuffer(4);
    var dv = new DataView(len);
    dv.setInt32(0, cnt.size, false);
    var b = new Blob([hdr, len, cnt]);
    this.socket.send(b);
    return b.length;
}

TcpClient.prototype._send_obj = function(obj) {
    if (this.socket.readyState != 1)
    {
        return 0;
    }
    
    var hdr = new Uint8Array(1);
    hdr[0] = (this.PACKET_TYPE_OBJ << 5) | (this.LENGTH_TYPE_32B << 4) | (this.SERIALIZE_TYPE_JSON << 2);
    var cnt = new Blob([JSON.stringify(obj)]);
    var len = new ArrayBuffer(4);
    var dv = new DataView(len);
    dv.setInt32(0, cnt.size, false);
    var b = new Blob([hdr, len, cnt]);
    this.socket.send(b);
    return b.length;
}

TcpClient.prototype._send_cmd = function(cmd, params) {
    if (this.socket.readyState != 1)
    {
        return 0;
    }
    
    var hdr = new Uint8Array(1);
    hdr[0] = (this.PACKET_TYPE_CMD << 5) | (this.LENGTH_TYPE_32B << 4) | (this.SERIALIZE_TYPE_JSON << 2);
    var cta = new ArrayBuffer(4);
    var dv = new DataView(cta);
    dv.setInt32(0, cmd, false);
    var cnt = new Blob([JSON.stringify(params)]);
    var len = new ArrayBuffer(4);
    dv = new DataView(len);
    dv.setInt32(0, cnt.size + 4, false);
    var b = new Blob([hdr, len, cta, cnt]);
    this.socket.send(b);
    return b.length;
}

TcpClient.prototype._hb = function() {
    if (this.DATA_TYPE_PACKET != this.data_type || !this.timer)
    {
        return;
    }
    
    if (typeof(this.on_heartbeat) == 'function')
    {
        this.on_heartbeat();
    }

    return;
}

TcpClient.prototype.get_packet = function() {
    return (this.pkts) ? this.pkts.pop() : null;
}

TcpClient.prototype.get_stream = function() {
    var ret = this.stream;
    this.stream = '';
    return ret;
}

TcpClient.prototype.send = function() {
    if (!this.socket || this.socket.readyState != 1)
    {
        return -1;
    }
    
    if (this.client_type == this.DATA_TYPE_STREAM)
    {
        // stream data
        var data = arguments[0] ? arguments[0] : '';
        if (typeof(data) == 'string')
        {
            return this._send_stream(data);
        }
        else
        {
            return -1;
        }
    }
    
    else
    {
        // JSON data
        var v1 = arguments[0] ? arguments[0] : null;
        if (!v1)
        {
            return -1;
        }
        
        if (typeof(v1) == 'number')
        {
            // Command
            var v2 = arguments[1] ? arguments[1] : new Object();
            if (typeof(v2) == 'object')
            {
                this._send_cmd(v1, v2);
            }
            else
            {
                return -1;
            }
        }
        else if (typeof(v1) == 'object')
        {
            // Object
            this._send_obj(v1);
        }
        else if (typeof(v1) == 'string')
        {
            // Raw
            this._send_raw(v1);
        }
        else
        {
            return -1;
        }
    }
}
